############################################################################
##
## Este modulo contem rotinas usadas para 'daemonizar' scripts.
##
## Os scripts sao tornados daemons executando a funcao Daemonize, que recebe
## como parametros o nome do daemon (usado para criar/verificar o ficheiro
## PID), referencias para quatro funcoes (explicadas mais adiante) e um
## numero que sera' o tempo maximo em segundos que o script fica suspenso
## entre cada execucao do ciclo principal. Se este valor for negativo o
## ciclo so' e' executado uma vez, e se for undef a suspensao e' infinita,
## sendo que nesse caso o ciclo so' e' retomado atraves de um SIGALRM.
##
## A funcao Daemonize faz um fork do script, e termina de seguida. O processo
## criado com o fork continua em execucao mas totalmente sob controlo da
## funcao Daemonize. Dai' que pelo menos tres das quatro referencias para
## funcoes que sao passadas para a funcao Daemonize sao de especial
## importancia. Uma delas sera' executada antes de entrar no ciclo, outra
## sera' executada durante o ciclo, e outra sera' executada no fim do ciclo.
##
## O ciclo do daemon e' composto apenas pela chamada 'a funcao indicada
## como parametro e uma pausa tao longa quanto o indicado tambem nos
## parametros da funcao Daemonize. Esta pausa termina no fim do tempo ou
## quando o daemon recebe um SIGALRM, o que pode ser feito manualmente ou
## simplesmente executando novamente o script, que logo que chame a funcao
## Daemonize ela ira' verificar que o daemon ja' esta' em execucao e envia
## entao um SIGALRM.
##
## As referencias para as quatro funcoes passadas como parametro para a
## funcao Daemonize sao (por ordem):
##
##  1. A funcao a executar para mostrar uma pagina de ajuda se os argumentos
##     usados para iniciar o script forem invalidos ou se for usado o
##     argumento 'help';
##
##  2. Uma funcao a executar antes de entrar no ciclo. Note-se que qualquer
##     funcao que o script execute antes de chamar a funcao Daemonize ira'
##     ser executada no parent e nao no daemon, por isso qualquer rotina de
##     inicializacao que tenha que ser executada apos o fork deve ser
##     indicada como referencia neste parametro;
##
##   3. A funcao a executar dentro do ciclo;
##
##   4. Uma funcao a executar depois de terminar o ciclo.
##
############################################################################
package daemon;

use strict;
use warnings;

# pragma para capturar SIG events
use sigtrap qw ( die untrapped normal-signals
		 die untrapped error-signals
		 die untrapped old-interface-signals );

# Flags para detectar SIG events
my $terminated = 0;
my $alrmrcvd = 0;

# handlers para os SIG events QUIT, TERM e ALRM
$SIG{QUIT} = sub { $terminated = 2; };
$SIG{TERM} = sub { $terminated = 1; };
$SIG{ALRM} = sub { $alrmrcvd = 1; };

use PTconfig;    # Constantes de configuracao
use PTlog;       # Reporte de erros e de notificacoes

use POSIX 'setsid';
use Cwd 'chdir';

use exceptions;  # Exception handling via try{}except{}finally{};


############################################################################
# TERMINATED
#
# Retorna o valor da flag $terminated. Esta funcao pode ser usada na
# funcao do script usada no ciclo, por forma a saber se o ciclo esta' para
# terminar ou nao.
#
sub Terminated()
{
  return $terminated;
}
############################################################################

############################################################################
# SIGNALED
#
# Retorna True se tiver recebido um dos SIG events implementados.
#
sub Signaled()
{
  return $alrmrcvd || $terminated;
}
############################################################################

############################################################################
# CLEAR
#
# Limpa a flag $alrmrcvd
#
sub Clear()
{
  $alrmrcvd = 0;
  return 1;
}
############################################################################

############################################################################
# Sleep
#
# Suspende a execucao do daemon ate' que um dos SIG events seja recebido ou
# ate' que acabe o tempo indicado em $_[0].
#
# Parametros:
#    $_[0] = Timeout, em segundos (undef=infinito).
#
sub Sleep($)
{
  my $timeout = shift;
  
  do { defined $timeout ? sleep($timeout) : sleep; } unless Signaled();
  return $terminated ? 0 : Clear();
}
############################################################################

############################################################################
# CHECK COMMAND LINE OPTIONS
#
# Verifica as opcoes da linha de comandos. Retorna True (1) se a opcao
# 'stop' foi indicada; retorna False (0) se a opcao 'start' foi indicada
# ou se nenhuma opcao foi especificada; retorna 'undef' em qualquer outra
# situacao.
#
sub CheckCommandLineOptions()
{
  my $args = join ' ',@ARGV;

  my $start = $args ? $args =~ m/^start$/i : 1;
  my $stop  = $args =~ m/^stop$/i;
  my $help  = $args =~ m/^help$/i;

  return 0 if $start;
  return 1 if $stop;
  return undef;
}
############################################################################

############################################################################
# IS RUNNING
#
# Testa se o daemon ja' esta' a ser executado procurando o respectivo
# ficheiro PID. Retorna zero ou o PID do daemon encontrado.
#
# Parametros:
#    $_[0] = caminho completo para o ficheiro PID
#
sub IsRunning($)
{
  my $pidfile = shift;
  
  # testa se o ficheiro PID indicado existe
  if (-f $pidfile)
  {
    # le^ o ficheiro
    open FILE, "< $pidfile";
    my $pid = <FILE>;
    close FILE;
    chomp $pid;

    # verifica a validade do PID lido
    if ($pid =~ m/^\d+$/)
    {
      # verifica se existe um processo com o PID lido
      if (kill 0, $pid)
      {
        return $pid;
      }
    }
  }
  
  return 0;
}
############################################################################

############################################################################
# CREATE DAEMON
#
# Inicializa o daemon num child process, abortando o parent. Retorna True
# em caso de sucesso ou lanca uma excepcao em caso de erro. Se o daemon for
# lancado correctamente, aborta o parent com exit(0).
#
# Parametros:
#    $_[0] = caminho completo para o ficheiro PID
#
sub CreateDaemon($)
{
  my $pidfile = shift;

  FORK:
  {
    # tenta lancar o daemon num child process
    my $pid = fork;

    if($pid)
    {
      # o daemon foi lancado correctamente, por isso
      # termina o processo que o lancou.
      exit (0);
    }
    elsif (defined $pid)
    {
      # este e' o ponto onde o child process comeca a ser
      # executado, tentando criar um process group e
      # destacar-se do terminal de controlo
      if (setsid() >= 0)
      {
        # repete o fork para assegurar a lideranca da sessao
        $pid = fork;
        exit(0) if $pid;
        throw exception "FORK", "Fork failed: $!\n" unless defined $pid;

        # muda para a raiz por forma a possibilitar umount do filesystem
        chdir '/' or throw exception "SYSTEM", "chdir failed";

        # limpa a umask para permitir ao daemon criar ficheiros como
        # seja necessario
        umask 0;

        # tenta criar o ficheiro $pidfile;
        # retorna True se conseguir ou lanca uma excepcao se falhar.
        if (system "echo $$ > $pidfile")
        {
          throw exception "SYSTEM", "Failed to create PID file $pidfile";
        }
        else
        {
          return 1;
        }
      }
      else
      {
        throw exception "POSIX", 'setsid failed. Daemon not started!';
      }
    }
    elsif ($! =~ /No more process/)
    {
      # erro recuperavel; aguarda 3 segundos e tenta novamente.
      sleep 3;
      redo FORK;
    }
    else
    {
      throw exception "FORK", "Fork failed: $!\n";
    }
  }
}
############################################################################

############################################################################
# Init
#
# Verifica se o daemon ja' esta' em execucao ou inicia-o. Retorna True se
# o daemon foi iniciado correctamente ou False se as opcoes da linha de
# comandos estavam incorrectas ou se incluiam a opcao -help.
#
# Se o daemon ja' estiver em execucao, envia-lhe um SIGALRM ou um SIGTERM
# conforme as opcoes da linha de comandos forem, respectivamente, -start ou
# -stop e aborta a execucao do script com exit(0).
#
# Se o daemon ainda nao estiver em execucao, aborta o script com exit(0) se
# conseguir lancar o daemon correctamente ou cria uma exception se falhar.
#
# Esta funcao so' retorna True no child process (i.e. no daemon correctamente
# lancado) e so' retorna False no parent process e apenas para que o script
# possa apresentar a pagina de ajuda antes de abortar.
#
# Parametros:
#    $_[0] = caminho completo para o ficheiro PID
#
sub Init($)
{
  my $pidfile = shift;
  my $options = CheckCommandLineOptions();

  if (defined $options)
  {
    if (my $pid = IsRunning($pidfile))
    {
      # se o daemon ja' esta' activo, envia-lhe um SIGTERM se a opcao
      # -stop foi usada, ou um SIGALRM (wakeup) se a opcao -start foi
      # implicita ou explicitamente usada.
      $options ? kill('TERM',$pid) : kill('ALRM',$pid);
      exit(0);
    }
    else
    {
      if ($options) # opcao -stop foi usada
      {
        exit(0);
      }
      
      return CreateDaemon($pidfile);
    }
  }
  else
  {
    return 0;
  }
}
############################################################################

############################################################################
# DAEMONIZE
#
# Inicializa o daemon. Nao retorna nunca, mas se lancado correctamente ira'
# executar ciclicamente a funcao indicada em $_[2], suspendendo a execucao
# do ciclo ate' que seja recebido um SIGALRM ou que passe o tempo indicado
# em $_[5].
#
# Parametros:
#    $_[0] = Nome do daemon (para criar/verificar o ficheiro PID)
#    $_[1] = Referencia para a funcao que retorna o texto da pagina de ajuda
#    $_[2] = Referencia para uma funcao a executar antes de entrar no ciclo
#    $_[3] = Referencia para a funcao a executar ciclicamente
#    $_[4] = Referencia para uma funcao a executar quando terminar o ciclo
#    $_[5] = Tempo maximo (segundos) de espera entre cada execucao do ciclo
#            (undef=infinito). Se negativo, corre apenas uma vez e termina.
#
sub Daemonize($$&&&$)
{
  my ($name, $help, $init, $run, $final, $timeout) = @_;
  my $pidfile = $PTconfig::PTPATH_PID."/$name.pid";
  my $exitcode = 0;
  
  try {
    if (Init($pidfile))
    {
      PTlog::LogNotice("Daemon $name started with pid $$!");
    }
    else
    {
      if (defined $help)
      { print &$help(); }
      else
      { print "Usage: $name [ start | stop | help ]\n"; }
      
      exit(0);
    }
    
    if (defined $init)
    { &$init(); }

    try {
      while (!($terminated))
      {
        &$run();
        last if $timeout < 0;
        Sleep($timeout);
      }
    }
    finally {
      if (defined $final)
      { &$final(); }
    };

    if ($terminated == 1)
    { PTlog::LogNotice("Daemon $name terminated on SIGTERM!"); }
    else
    { PTlog::LogNotice("Daemon $name terminated on SIGQUIT!"); }
  }
  except
  {
    PTlog::LogEmergency("Daemon $name: ".shift);
    $exitcode = 1;
  }
  finally {
    eval { system "rm $pidfile"; };
  };

  exit($exitcode);
}
############################################################################

1;
